<?php

namespace pmvc\ioc;

use Exception;
use ReflectionClass;

class ObjectFactory {

	private $parent			= NULL;
	private $objects		= Array();
	private $objectsById	= Array();
	private $descriptors	= Array();

	/**
	 * Creates the ObjectFactory with the given
	 * parent.
	 * @param $parent the parent
	 */
	public function ObjectFactory($parent=NULL) {
		$this->parent = $parent;
	}

	/**
	 * Adds an object descriptor to the {@link ObjectFactory}
	 * @param ObjectDescriptor $desc
	 */
	public function addObjectDescriptor($desc) {
		if (array_key_exists($desc->id, $this->descriptors)) {
			throw new Exception(
				"Object with id ".$desc->id." exists in this ObjectFactory");
		}
		$this->descriptors[$desc->id] = $desc;
	}

	/**
	 * Destructor.
	 */
	public function __destruct() {
		$this->runOnObjects(
			function($o, $f) { return ($o instanceof DestroyingObject); },
			function($o, $f) { return $o->destroy($f); },
			$this
		);
	}

	/**
	 * Gets an object from the factory using the given filter function.
	 * @param callback $filterFunc the filter function
	 */
	public function getObject($filterFunc) {
		foreach ($this->objects as $object) {
			if ($filterFunc($object)) {
				return $object;
			}
		}
		return NULL;
	}

	/**
	 * Gets an object from the factory of the given type.
	 * @param string $type the type name
	 */
	public function getObjectOfType($type) {
		return $this->getObject(function($obj)use($type) {
			return is_a($obj, $type);
		});
	}

	/**
	 * Gets an object from the factory by it's id.
	 * @param string $id the objects id
	 */
	public function getObjectById($id) {

		// check for existing object
		if (array_key_exists($id, $this->objectsById)) {
			return $this->objectsById[$id];
			
		// check parent factory
		} else if (!array_key_exists($id, $this->descriptors)
			&& $this->parent!=NULL) {
			return $this->parent->getObjectById($id);

		// if it's a singleton we create it every time
		} else if (array_key_exists($id, $this->descriptors)
			&& $this->descriptors[$id]->singleton) {
			$object = $this->instantiate($this->descriptors[$id]);
			array_push($this->objects, $object);
			return $object;

		// non singletons we check
		} else if (array_key_exists($id, $this->descriptors)
			&& !$this->descriptors[$id]->singleton) {
			$this->objectsById[$id] = $this->instantiate($this->descriptors[$id]);
			array_push($this->objects, $this->objectsById[$id]);
			return $this->objectsById[$id];
		}

		// we can't help you, sorry
		throw new Exception(
			"Object with id ".$id." not found");
	}

	/**
	 * Creates an object for the given descriptor.
	 * @param ObjectDescriptor $desc
	 */
	private function instantiate($desc) {

		// check for it's dependencies first
		$propDeps = Array();
		foreach ($desc->properties as $prop) {
			$propDeps[$prop->name] = ($prop->value!=NULL)
				? $prop->value : $this->getObjectById($prop->refId);
		}

		// check for constructor dependencies next
		$ctrDeps = Array();
		foreach ($desc->constructorArgs as $prop) {
			$ctrDeps[] = ($prop->value!=NULL)
				? $prop->value : $this->getObjectById($prop->refId);
		}

		// create the reflection class
		$refClass = new ReflectionClass($desc->class);

		// instantiate the object
		$object = (count($ctrDeps)>0)
			? $refClass->newInstanceArgs($ctrDeps)
			: $refClass->newInstance();

		// notify all ObjectFactoryListeners
		foreach ($this->objects as $obj) {
			if ($obj instanceof ObjectFactoryListener) {
				$ret = $obj->onObjectCreated($object, $desc->id, $this);
				if (isset($ret) && $ret!==FALSE && $ret!=NULL) {
					$object = $ret;
				}
			}
		}

		// populate it's properties
		foreach ($propDeps as $propName => $propValue) {

			// try the setter
			if ($refClass->hasMethod("set".$propName)) {
				$method = $refClass->getMethod("set".$propName);
				$method->invoke($object, $propValue);

			// try the property
			} else if ($refClass->hasProperty($propName)) {
				$prop = $refClass->getProperty($propName);
				$prop->setValue($object, $propValue);

			// zomg
			} else {
				throw new Exception(
					"No property or method named ".$propDep->name
					." found for type: ".$refClass->class);
			}
		}

		// initialize it if it needs to be
		if ($object instanceof InitializingObject) {
			$object->initialize($this);
		}

		// return the object
		return $object;
	}

	/**
	 * Runs the given $runFunc on objects that have
	 * a return value when passed to the given $filterFunc.
	 * The first argument sent to the $runFunc will be
	 * the object, subsequent arguments come from the
	 * $args.
	 * @param $filterFunc the filter function
	 * @param $runFunc the run function
	 * @param $args... arguments to send to the function
	 */
	public function runOnObjects() {
		$args 		= func_get_args();
		$filterFunc = array_shift($args);
		$runFunc 	= array_shift($args);
		foreach ($this->objects as $object) {
			array_unshift($args, $object);
			if (call_user_func_array($filterFunc, $args)===TRUE) {
				call_user_func_array($runFunc, $args);
			}
			array_shift($args);
		}
	}
	
}

interface InitializingObject {

	/**
	 * Called on objects that implement this interface
	 * when the ObjectFactory decides to initialize an
	 * object that it has created.  Objects are initialized
	 * after they have been created and properties have
	 * been set.
	 * @param $objectFactory the factory
	 */
	function initialize($objectFactory);
}

interface DestroyingObject {

	/**
	 * Called on objects that implement this interface
	 * when the ObjectFactory decides to destroy an
	 * object that it has created.
	 * @param $objectFactory the factory
	 */
	function destroy($objectFactory);
}

interface ObjectFactoryListener {

	/**
	 * Called when a new object is created.  This method
	 * is called before the object's properties are set.
	 * If the method returns a value (other than NULL or
	 * FALSE) it's value is used for intialzing and put
	 * into the ObjectFactory - allowing for the proxying
	 * of objects within the ObjectFactory.
	 * @param $object the object
	 * @param $id the object's id
	 * @param $objectFactory the factory it was created by
	 * @return [optional] an object to replace the created
	 * object within the ObjectFactory.
	 */
	function onObjectCreated($object, $id, $objectFactory);
}

class ObjectDescriptor {
	public function ObjectDescriptor($id, $class, $properties=Array(), $constructorArgs=Array(), $singleton=FALSE) {
		$this->id				= $id;
		$this->class			= $class;
		$this->properties		= $properties;
		$this->constructorArgs	= $constructorArgs;
		$this->singleton		= $singleton;
	}
	public $id				= NULL;
	public $class 			= NULL;
	public $singleton		= TRUE;
	public $constructorArgs	= Array();
	public $properties		= Array();
}

class PropertyDescriptor {
	public function PropertyDescriptor($name, $value, $refId=NULL) {
		$this->name		= $name;
		$this->value	= $value;
		$this->refId	= $refId;
	}
	public $name	= NULL;
	public $value	= NULL;
	public $refId	= NULL;
}

?>